最近因為要結婚了,所以有接觸婚攝這類的事情,然後發現要跟攝影師討論照相風格是一件麻煩的事情,若不是很能掌握照相風格的,就很難去描述你想要的那種風格,因此我們都會藉由挑選範例照片來輔助描述。
不過照篇挑選也是要做註記,所以自己寫了一個挑選工具,自動去下載一些有名的攝影師作品,然後只要動動手指,往上或往下標記喜歡或不喜歡即可。
而那時候遇到了一個問題,就是 fb graph api 只允許抓取粉絲頁的照片,個人版的照片是抓不到的,那我們今天的主題就來抓取個人版照片吧。
我們這次來用鯊魚個人網站的婚紗相簿來 demo,打開網頁之後,就會看到照片,往下捲動到最底下之後,會動態出現更多照片。
應該有注意到,在一開始開網頁的時候照片的讀取是直接出現的,也就是説在一開始的 request 他就會將 images 附在 response 的 html 裡面。而下方讀取更多是透過 ajax 呼叫 request,然後再使用 js 將取得的 response 內的照片附加在網頁上。我們可以透過 dev tools 來確認一下這點。
透過 dev tools 觀察後,發現第一個 request 的照片會放在 #u_0_89
裡面(不過這邊我在測試的時候,發現不同的 cookie 會有不同的 ID,所以比較正確的做法會是去取得 album_photos_pagelet
的 container_id
,但這邊就先不多贅),而且是註解起來的內容,也就說我們等等研究的時候需要先將這邊註解取消掉。
再來看一下取得更多的 request,發現送出去的 query string 裡面有一個很大串的 data
,這裡面有個很有趣的數字 fetch_size
,這個看起來有 pagination size 的感覺,等等我們研究時可以來測試看看。同時也注意到在 data
裡面有三個有關聯的參數 set
、profile_id
、last_fbid
,set
、profile_id
是由第一個 request 的網址延伸來的,而 last_fbid
則是第個 response 所給的最後一張圖片的 id。
經過探訪後,我們只要搞定第一網址的 request 和取得更多照片的 request 大概就沒有問題了。
要取得所有個人相簿照片,那麼會拆解成以下四個步驟:
接著我們模擬一下送出的 request,一樣用刪去法把不需要送出的 cookie 和 query string 都拿掉,確定是可以拿到結果。
接下來我們將取得的 html 程式碼去掉註解,然後丟到一個空的 html file 看看,同時去 select 圖片位置,這邊要注意到,因為圖片是放在 css 使用 background-image,這邊他有將值做 css escape,所以我們需要 replace 一些被 escape 的符號,確定是可以拿到圖片列表沒問題,不過等等我們實作的時候還要將最後一張圖片的 last_fbid
取出。
先用 postman 將 request 模擬一下,這邊要注意到,若 request headers 的 accept-encoding 有帶入 gzip, deflate, br
,則 response 會回給我們 content-type 為 application/octet-stream
stream buffer,所以我們這邊的 headers 記得把 accept-encoding 拿掉,然後確定可以取道結果就沒問題了。
另外我們也來看一下 query string 的 params,一樣刪去法把不必要的東西拿掉,這樣會剩下 data
、__a
、ajaxpipe_fetch_stream
,關鍵點會在 data
,裡面要包含有剛剛我們拿到的 last_fbid
、set
、profile_id
,另外我們也可以來調整一下 fetch_size
,確定真的是 page size,那我們的分頁就不用 call 那麼多次了。
接下來我們來解析一下分頁的 response,把前面的贅字拿掉後是一個 json format,而包含圖片的 html 會在 payload
裡面。那我們一樣打開一個空白的 html 來解析看看,確定可以拿到圖片就沒問題了。
先來製作 init 的 request finction,這個 function 會收 url 參數和 callback,然後先將等等分頁 request 要用的 albumId 先從 url parse 出來。接著我們去解析取得的 response,將註解拿掉,再將 css 的 escape 取代掉。取完圖片以後,我們再將最後一張圖片的 id 拿出來。最後我們在呼叫等等要實作的 getMoreImages function。
function getInitImages(url, callback){
var albumId = parse(url, true).query.set
request(url, (err, res, body)=>{
var $ = cheerio.load(body)
var html = $('#u_0_89').html()
var $ = cheerio.load(html.replace('<!-- ','').replace(' -->',''))
var images = $('._pq3').map((index, obj) => {
return $(obj).attr('style').match(/url\('(.+)'\)/)[1].replace('\\\\3a ', ':').replace(/\\\\3d /g, '=').replace('\\\\26 ', '&')
}).get()
var last = images[images.length-1].split('_')[1]
getMoreImages(albumId, last, (moreImages)=>{
callback(images.concat(moreImages))
})
})
}
取得更多圖片的 request function,會收 init request 丟過來的 albumId 和 last_id,然後我們要組合出要發出 request 的 query string,尤其是 data
的部分。拿到 response 之後,將前面贅字拿掉做 json parse,然後取得 payload
裡面的 html,就能夠將 image select 出來,最後在做 css escape 的 replace 就完成了。
function getMoreImages(albumId, last, callback){
var profile_id = albumId.split('.')[3]
var options = {
method: 'GET',
url: 'https://www.facebook.com/ajax/pagelet/generic.php/TimelinePhotosAlbumPagelet',
qs: {
data: `{"last_fbid":${last},"fetch_size":1000,"set":"${albumId}","__a":"1","profile_id":${profile_id}}`,
__a: '1',
ajaxpipe_fetch_stream: '1'
},
};
request(options, function(error, response, body) {
if (error) throw new Error(error);
var data = JSON.parse(body.replace('for (;;);', ''))
var $ = cheerio.load(data.payload)
var images = $('._pq3').map((index, obj) => {
return $(obj).attr('style').match(/url\('(.+)'\)/)[1].replace('\\3a ', ':').replace(/\\3d /g, '=').replace('\\26 ', '&')
}).get()
callback(images);
});
}
先將 url 定義好,接下來直接呼叫 getInitImages 就可以了。
var url = 'https://www.facebook.com/SharkJiang.Wedding/media_set?set=a.1381648032142132.1073741831.100008908444745&type=3';
getInitImages(url, (images)=>{
console.log(images);
})
var request = require("request").defaults({
headers: {
'cookie': 'xxxxxxxxxx',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36',
}
});
var cheerio = require("cheerio");
const parse = require('url-parse');
var url = 'https://www.facebook.com/SharkJiang.Wedding/media_set?set=a.1381648032142132.1073741831.100008908444745&type=3';
getInitImages(url, (images)=>{
console.log(images);
})
function getInitImages(url, callback){
var albumId = parse(url, true).query.set
request(url, (err, res, body)=>{
var $ = cheerio.load(body)
var html = $('#u_0_89').html()
var $ = cheerio.load(html.replace('<!-- ','').replace(' -->',''))
var images = $('._pq3').map((index, obj) => {
return $(obj).attr('style').match(/url\('(.+)'\)/)[1].replace('\\\\3a ', ':').replace(/\\\\3d /g, '=').replace('\\\\26 ', '&')
}).get()
var last = images[images.length-1].split('_')[1]
getMoreImages(albumId, last, (moreImages)=>{
callback(images.concat(moreImages))
})
})
}
function getMoreImages(albumId, last, callback){
var profile_id = albumId.split('.')[3]
var options = {
method: 'GET',
url: 'https://www.facebook.com/ajax/pagelet/generic.php/TimelinePhotosAlbumPagelet',
qs: {
data: `{"last_fbid":${last},"fetch_size":1000,"set":"${albumId}","__a":"1","profile_id":${profile_id}}`,
__a: '1',
ajaxpipe_fetch_stream: '1'
},
};
request(options, function(error, response, body) {
if (error) throw new Error(error);
var data = JSON.parse(body.replace('for (;;);', ''))
var $ = cheerio.load(data.payload)
var images = $('._pq3').map((index, obj) => {
return $(obj).attr('style').match(/url\('(.+)'\)/)[1].replace('\\3a ', ':').replace(/\\3d /g, '=').replace('\\26 ', '&')
}).get()
callback(images);
});
}
FB graph api 很方便沒錯,但朕不給你的你不能要,所以其實有很多資料並無法從 api 取得,當遇到這個時候,就請發揮所見即所得的精神,自幹一個爬蟲 api 出來,那麼就能創造出別人無法創造的價值。